/*
 * Allwinner H3 UART
 *
 * Copyright (c) 2025 yanl1229 <yanl1229@163.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "qemu/osdep.h"
#include "hw/char/serial.h"
#include "chardev/char-serial.h"
#include "hw/char/allwinner-h3-uart.h"
#include "hw/irq.h"
#include "hw/qdev-properties.h"
#include "qemu/log.h"
#include "qemu/module.h"

#ifndef DEBUG
#define DEBUG 0
#endif

#define DB_PRINT_L(lvl, fmt, args...) do { \
    if (DEBUG >= lvl) { \
        qemu_log("%s: " fmt, __func__, ## args); \
    } \
} while (0)

#define DB_PRINT(fmt, args...) DB_PRINT_L(1, fmt, ## args)

static inline void recv_fifo_put(AllwinnerH3UartState *s, uint8_t chr)
{
    /* Receive overruns do not overwrite FIFO contents. */
    if (!fifo8_is_full(&s->recv_fifo)) {
        fifo8_push(&s->recv_fifo, chr);
    } else {
        s->uart_lsr |= UART_LSR_OE;
    }
}

static void serial_write_fcr(AllwinnerH3UartState *s, uint8_t val)
{
    /* Set fcr - val only has the bits that are supposed to "stick" */
    s->uart_fcr = val;

    if (val & UART_FCR_FE) {
        s->uart_iir |= UART_IIR_FE;
        /* Set recv_fifo trigger Level */
        switch (val & 0xC0) {
        case UART_FCR_ITL_1:
            s->recv_fifo_itl = 1;
            break;
        case UART_FCR_ITL_2:
            s->recv_fifo_itl = 4;
            break;
        case UART_FCR_ITL_3:
            s->recv_fifo_itl = 8;
            break;
        case UART_FCR_ITL_4:
            s->recv_fifo_itl = 14;
            break;
        }
    } else {
        s->uart_iir &= ~UART_IIR_FE;
    }
}

static void serial_update_irq(AllwinnerH3UartState *s)
{
    uint8_t tmp_iir = UART_IIR_NO_INT;

    if ((s->uart_ier & UART_IER_RLSI) && (s->uart_lsr & UART_LSR_INT_ANY)) {
        tmp_iir = UART_IIR_RLSI;
    } else if ((s->uart_ier & UART_IER_RDI) && (s->uart_lsr & UART_LSR_DR) &&
               (!(s->uart_fcr & UART_FCR_FE) ||
                s->recv_fifo.num >= s->recv_fifo_itl)) {
        tmp_iir = UART_IIR_RDI;
    } else if ((s->uart_ier & UART_IER_THRI) && s->thr_ipending) {
        tmp_iir = UART_IIR_THRI;
    } else if ((s->uart_ier & UART_IER_MSI) && (s->uart_msr & UART_MSR_ANY_DELTA)) {
        tmp_iir = UART_IIR_MSI;
    }

    s->uart_iir = tmp_iir | (s->uart_iir & 0xF0);

    if (tmp_iir != UART_IIR_NO_INT) {
        qemu_irq_raise(s->irq);
    } else {
        qemu_irq_lower(s->irq);
    }
}

static int allwinner_h3_uart_can_receive(void *opaque)
{
    AllwinnerH3UartState *s = opaque;

    if(s->uart_fcr & UART_FCR_FE) {
        if (s->recv_fifo.num < UART_FIFO_SIZE) {
            return (s->recv_fifo.num <= s->recv_fifo_itl) ?
                        s->recv_fifo_itl - s->recv_fifo.num : 1;
        } else {
            return 0;
        }
    } else {
        return !(s->uart_lsr & UART_LSR_DR);
    }

    return 0;
}

static void allwinner_h3_uart_receive(void *opaque, const uint8_t *buf, int size)
{
    AllwinnerH3UartState *s = opaque;
    if(s->uart_fcr & UART_FCR_FE) {
        int i;
        for (i = 0; i < size; i++) {
            recv_fifo_put(s, buf[i]);
        }
        s->uart_lsr |= UART_LSR_DR;
    } else {
        if (s->uart_lsr & UART_LSR_DR)
            s->uart_lsr |= UART_LSR_OE;
        s->uart_rbr = buf[0];
        s->uart_lsr |= UART_LSR_DR;
    }
    serial_update_irq(s);
}

static void serial_xmit(AllwinnerH3UartState *s)
{
    do {
        assert(!(s->uart_lsr & UART_LSR_TEMT));

        assert(!(s->uart_lsr & UART_LSR_THRE));

        if (s->uart_fcr & UART_FCR_FE) {
            assert(!fifo8_is_empty(&s->xmit_fifo));
            s->tsr = fifo8_pop(&s->xmit_fifo);
            if (!s->xmit_fifo.num) {
                s->uart_lsr |= UART_LSR_THRE;
            }
        } else {
            s->tsr = (uint8_t)s->uart_thr;
            s->uart_lsr |= UART_LSR_THRE;
        }
        if ((s->uart_lsr & UART_LSR_THRE) && !s->thr_ipending) {
            s->thr_ipending = 1;
            serial_update_irq(s);
        }


        if (s->uart_mcr & UART_MCR_LOOP) {
            /* in loopback mode, say that we just received a char */
            allwinner_h3_uart_receive(s, &s->tsr, 1);
        } else {
            qemu_chr_fe_write(&s->chr, &s->tsr, 1);
        }
        /* Transmit another byte if it is already available. It is only
           possible when FIFO is enabled and not empty. */
    } while (!(s->uart_lsr & UART_LSR_THRE));


    s->uart_lsr |= UART_LSR_TEMT;
}

static void serial_update_parameters(AllwinnerH3UartState *s)
{
    float speed;
    int parity, data_bits, stop_bits, frame_size;
    QEMUSerialSetParams ssp;

    /* Start bit. */
    frame_size = 1;
    if (s->uart_lcr & 0x08) {
        /* Parity bit. */
        frame_size++;
        if (s->uart_lcr & 0x10)
            parity = 'E';
        else
            parity = 'O';
    } else {
            parity = 'N';
    }
    if (s->uart_lcr & 0x04) {
        stop_bits = 2;
    } else {
        stop_bits = 1;
    }

    data_bits = (s->uart_lcr & 0x03) + 5;
    frame_size += data_bits + stop_bits;
    /* Zero divisor should give about 3500 baud */
    speed = (s->divider == 0) ? 3500 : (float) s->baudbase / s->divider;
    ssp.speed = speed;
    ssp.parity = parity;
    ssp.data_bits = data_bits;
    ssp.stop_bits = stop_bits;
    qemu_chr_fe_ioctl(&s->chr, CHR_IOCTL_SERIAL_SET_PARAMS, &ssp);

    DB_PRINT("speed=%.2f parity=%c data=%d stop=%d\n",
           speed, parity, data_bits, stop_bits);
}

static void allwinner_h3_uart_reset(DeviceState *dev)
{
    AllwinnerH3UartState *s = ALLWINNER_H3_UART(dev);

    s->uart_rbr = 0x00000000;
    s->uart_thr = 0x00000000;
    s->uart_dll = 0x00000000;
    s->uart_dlh = 0x00000000;
    s->uart_ier = 0x00000000;
    s->uart_iir = 0x00000001;
    s->uart_fcr = 0x00000000;
    s->uart_lcr = 0x00000000;
    s->uart_mcr = 0x00000000;
    s->uart_lsr = 0x00000000;
    s->uart_msr = 0x00000000;
    s->uart_sch = 0x00000000;
    s->uart_usr = 0x00000000;
    s->uart_tfl = 0x00000000;
    s->uart_rfl = 0x00000000;
    s->uart_halt = 0x00000000;
    qemu_set_irq(s->irq, 0);
}

static uint64_t allwinner_h3_uart_read(void *opaque, hwaddr addr,
                                       unsigned int size)
{
    AllwinnerH3UartState *s = opaque;
    uint32_t ret = 0;

    DB_PRINT("Read 0x%"HWADDR_PRIx"\n", addr);

    switch (addr) {
    case UART_RBR:
        if (s->uart_lcr & UART_LCR_DLAB) {
            ret = s->divider & 0xff;
        } else {
            if(s->uart_fcr & UART_FCR_FE) {
                ret = fifo8_is_empty(&s->recv_fifo) ?
                            0 : fifo8_pop(&s->recv_fifo);
                if (s->recv_fifo.num == 0) {
                    s->uart_lsr &= ~(UART_LSR_DR | UART_LSR_BI);
                }
            } else {
                ret = s->uart_rbr;
                s->uart_lsr &= ~(UART_LSR_DR | UART_LSR_BI);
            }
            serial_update_irq(s);
            if (!(s->uart_mcr & UART_MCR_LOOP)) {
                /* in loopback mode, don't receive any data */
                qemu_chr_fe_accept_input(&s->chr);
            }
        }
        break;
    case UART_IER:
        if (s->uart_lcr & UART_LCR_DLAB) {
            ret = (s->divider >> 8) & 0xff;
        } else {
            ret = s->uart_ier;
        }
        break;
    case UART_IIR:
        ret = s->uart_iir;
        if ((ret & UART_IIR_ID) == UART_IIR_THRI) {
            s->thr_ipending = 0;
            serial_update_irq(s);
        }
        break;
    case UART_LCR:
        ret = s->uart_lcr;
        break;
    case UART_LSR:
        ret = s->uart_lsr;
        /* Clear break and overrun interrupts */
        if (s->uart_lsr & (UART_LSR_BI|UART_LSR_OE)) {
            s->uart_lsr &= ~(UART_LSR_BI|UART_LSR_OE);
            serial_update_irq(s);
        }
        break;
    case UART_MCR:
        if (s->uart_mcr & UART_MCR_LOOP) {
            /* in loopback, the modem output pins are connected to the
               inputs */
            ret = (s->uart_mcr & 0x0c) << 4;
            ret |= (s->uart_mcr & 0x02) << 3;
            ret |= (s->uart_mcr & 0x01) << 5;
        } else {
            ret = s->uart_mcr;
            /* Clear delta bits & msr int after read, if they were set */
            if (s->uart_mcr & UART_MSR_ANY_DELTA) {
                s->uart_mcr &= 0xF0;
                serial_update_irq(s);
            }
        }
        break;
    default:
        qemu_log_mask(LOG_GUEST_ERROR,
                      "%s: Bad offset 0x%"HWADDR_PRIx"\n", __func__, addr);
        return 0;
    }

    return ret;
}

static void allwinner_h3_uart_write(void *opaque, hwaddr addr,
                                  uint64_t val64, unsigned int size)
{
    AllwinnerH3UartState *s = opaque;
    uint32_t value = val64;

    DB_PRINT("Write 0x%" PRIx32 ", 0x%"HWADDR_PRIx"\n", value, addr);

    switch (addr) {
    case UART_THR:
        if (s->uart_lcr & UART_LCR_DLAB) {
            if (size == 1) {
                s->divider = (s->divider & 0xff00) | value;
            } else {
                s->divider = value;
            }
            serial_update_parameters(s);
        } else {
            s->uart_thr = value;
            if(s->uart_fcr & UART_FCR_FE) {
                /* xmit overruns overwrite data, so make space if needed */
                if (fifo8_is_full(&s->xmit_fifo)) {
                    fifo8_pop(&s->xmit_fifo);
                }
                fifo8_push(&s->xmit_fifo, s->uart_thr);
            }
            s->thr_ipending = 0;
            s->uart_lsr &= ~UART_LSR_THRE;
            s->uart_lsr &= ~UART_LSR_TEMT;
            serial_update_irq(s);
            serial_xmit(s);
        }
        break;
    case UART_FCR:
        if ((value ^ s->uart_fcr) & UART_FCR_FE) {
            value |= UART_FCR_XFR | UART_FCR_RFR;
        }

        /* FIFO clear */
        if (value & UART_FCR_RFR) {
            s->uart_lsr &= ~(UART_LSR_DR | UART_LSR_BI);
            fifo8_reset(&s->recv_fifo);
        }
        if (value & UART_FCR_XFR) {
            s->uart_lsr |= UART_LSR_THRE;
            s->thr_ipending = 1;
            fifo8_reset(&s->xmit_fifo);
        }

        serial_write_fcr(s, value & 0xC9);
        serial_update_irq(s);
        break;
    case UART_LCR:
        {
            int break_enable;
            s->uart_lcr = value;
            serial_update_parameters(s);
            break_enable = (value >> 6) & 1;
            if (break_enable != s->last_break_enable) {
                s->last_break_enable = break_enable;
                qemu_chr_fe_ioctl(&s->chr, CHR_IOCTL_SERIAL_SET_BREAK,
                                  &break_enable);
            }
        }
        break;
    default:
        qemu_log_mask(LOG_GUEST_ERROR,
                      "%s: Bad offset 0x%"HWADDR_PRIx"\n", __func__, addr);
    }
}

static const MemoryRegionOps allwinner_h3_uart_ops = {
    .read = allwinner_h3_uart_read,
    .write = allwinner_h3_uart_write,
    .endianness = DEVICE_NATIVE_ENDIAN,
};

static Property allwinner_h3_uart_properties[] = {
    DEFINE_PROP_CHR("chardev", AllwinnerH3UartState, chr),
    DEFINE_PROP_END_OF_LIST(),
};

static void allwinner_h3_uart_init(Object *obj)
{
    AllwinnerH3UartState *s = ALLWINNER_H3_UART(obj);

    sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irq);

    memory_region_init_io(&s->mmio, obj, &allwinner_h3_uart_ops, s,
                          TYPE_ALLWINNER_H3_UART, 0x400);
    sysbus_init_mmio(SYS_BUS_DEVICE(obj), &s->mmio);
}

static void allwinner_h3_uart_realize(DeviceState *dev, Error **errp)
{
    AllwinnerH3UartState *s = ALLWINNER_H3_UART(dev);

    qemu_chr_fe_set_handlers(&s->chr, allwinner_h3_uart_can_receive,
                             allwinner_h3_uart_receive, NULL, NULL,
                             s, NULL, true);
    fifo8_create(&s->recv_fifo, UART_FIFO_SIZE);
    fifo8_create(&s->xmit_fifo, UART_FIFO_SIZE);
}

static void allwinner_h3_uart_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);

    dc->reset = allwinner_h3_uart_reset;
    device_class_set_props(dc, allwinner_h3_uart_properties);
    dc->realize = allwinner_h3_uart_realize;
}

static const TypeInfo allwinner_h3_uart_info = {
    .name          = TYPE_ALLWINNER_H3_UART,
    .parent        = TYPE_SYS_BUS_DEVICE,
    .instance_size = sizeof(AllwinnerH3UartState),
    .instance_init = allwinner_h3_uart_init,
    .class_init    = allwinner_h3_uart_class_init,
};

static void allwinner_h3_uart_register_types(void)
{
    type_register_static(&allwinner_h3_uart_info);
}

type_init(allwinner_h3_uart_register_types)
